﻿using Android.App;
using Android.Content.PM;
using Android.Graphics;
using Android.Hardware.Camera2;
using Android.Hardware.Camera2.Params;
using Android.Media;
using Android.OS;
using Android.Runtime;
using Android.Util;
using Android.Views;
using Android.Widget;

using Java.Util;
using Java.Util.Concurrent;
using System;
using System.Collections.Generic;
using System.Linq;

namespace CameraDemo
{
    public class Camera2BasicFragment : Fragment
    {


        /** Tag for the {@link Log}. */
        private static String TAG = "TfLiteCameraDemo";

        private static String FRAGMENT_DIALOG = "dialog";

        private static String HANDLE_THREAD_NAME = "CameraBackground";

        private static int PERMISSIONS_REQUEST_CODE = 1;

        private static Object @lock = new Object();
        private bool runClassifier = false;
        private bool checkedPermissions = false;
        private TextView textView;
     

        /** Max preview width that is guaranteed by Camera2 API */
        private static int MAX_PREVIEW_WIDTH = 1920;

        /** Max preview height that is guaranteed by Camera2 API */
        private static int MAX_PREVIEW_HEIGHT = 1080;

        /** ID of the current {@link CameraDevice}. */
        private String cameraId;

        /** An {@link AutoFitTextureView} for camera preview. */
        private AutoFitTextureView textureView;

        /** A {@link CameraCaptureSession } for camera preview. */
        private CameraCaptureSession captureSession;

        /** A reference to the opened {@link CameraDevice}. */
        private CameraDevice cameraDevice;

        /** The {@link android.util.Size} of camera preview. */
        private Size previewSize;

        /** {@link CameraDevice.StateCallback} is called when {@link CameraDevice} changes its state. */
        //    private  CameraDevice.StateCallback stateCallback =
        //        new CameraDevice.StateCallback() {

        //      //  //@Override
        //          public void onOpened(@NonNull CameraDevice currentCameraDevice)
        //    {
        //        // This method is called when the camera is opened.  We start camera preview here.
        //        cameraOpenCloseLock.release();
        //        cameraDevice = currentCameraDevice;
        //        createCameraPreviewSession();
        //    }

        //  //  //@Override
        //        public void onDisconnected(@NonNull CameraDevice currentCameraDevice)
        //    {
        //        cameraOpenCloseLock.release();
        //        currentCameraDevice.close();
        //        cameraDevice = null;
        //    }

        //  //  //@Override
        //        public void onError(@NonNull CameraDevice currentCameraDevice, int error)
        //    {
        //        cameraOpenCloseLock.release();
        //        currentCameraDevice.close();
        //        cameraDevice = null;
        //        Activity activity = Activity
        //        if (null != activity)
        //        {
        //            activity.finish();
        //        }
        //    }
        //};

        /** An additional thread for running tasks that shouldn't block the UI. */
        private HandlerThread backgroundThread;

        /** A {@link Handler} for running tasks in the background. */
        private Handler backgroundHandler;

        /** An {@link ImageReader} that handles image capture. */
        private ImageReader imageReader;

        /** {@link CaptureRequest.Builder} for the camera preview */
        private CaptureRequest.Builder previewRequestBuilder;

        /** {@link CaptureRequest} generated by {@link #previewRequestBuilder} */
        private CaptureRequest previewRequest;

        /** A {@link Semaphore} to prevent the app from exiting before closing the camera. */
        private Semaphore cameraOpenCloseLock = new Semaphore(1);



        public Camera2BasicFragment()
        {
            periodicClassify =
              new Java.Lang.Runnable(() =>
              {
                  lock (@lock)
                  {
                      //if (runClassifier)
                      //{
                      //    classifyFrame();
                      //}
                  }
                  backgroundHandler.Post(periodicClassify);
              });
        }

        /**
         * Shows a {@link Toast} on the UI thread for the classification results.
         *
         * @param text The message to show
         */
        private void showToast(System.String text)
        {
            Activity activity = Activity;
            if (activity != null)
            {
                activity.RunOnUiThread(
                    new Java.Lang.Runnable(() => { textView.Text = (text); }));

            }
        }

        /**
         * Resizes image.
         *
         * Attempting to use too large a preview size could  exceed the camera bus' bandwidth limitation,
         * resulting in gorgeous previews but the storage of garbage capture data.
         *
         * Given {@code choices} of {@code Size}s supported by a camera, choose the smallest one that is
         * at least as large as the respective texture view size, and that is at most as large as the
         * respective max size, and whose aspect ratio matches with the specified value. If such size
         * doesn't exist, choose the largest one that is at most as large as the respective max size, and
         * whose aspect ratio matches with the specified value.
         *
         * @param choices The list of sizes that the camera supports for the intended output class
         * @param textureViewWidth The width of the texture view relative to sensor coordinate
         * @param textureViewHeight The height of the texture view relative to sensor coordinate
         * @param maxWidth The maximum width that can be chosen
         * @param maxHeight The maximum height that can be chosen
         * @param aspectRatio The aspect ratio
         * @return The optimal {@code Size}, or an arbitrary one if none were big enough
         */
        private static Size chooseOptimalSize(
            Size[] choices,
            int textureViewWidth,
            int textureViewHeight,
            int maxWidth,
            int maxHeight,
            Size aspectRatio)
        {

            // Collect the supported resolutions that are at least as big as the preview Surface
            List<Size> bigEnough = new List<Size>();
            // Collect the supported resolutions that are smaller than the preview Surface
            List<Size> notBigEnough = new List<Size>();
            int w = aspectRatio.Width;
            int h = aspectRatio.Height;
            foreach (Size option in choices)
            {
                if (option.Width <= maxWidth
                    && option.Height <= maxHeight
                    && option.Height == option.Width * h / w)
                {
                    if (option.Width >= textureViewWidth && option.Height >= textureViewHeight)
                    {
                        bigEnough.Add(option);
                    }
                    else
                    {
                        notBigEnough.Add(option);
                    }
                }
            }

            // Pick the smallest of those big enough. If there is no one big enough, pick the
            // largest of those not big enough.
            if (bigEnough.Count > 0)
            {
                return (Size)Collections.Min(bigEnough, new CompareSizesByArea());
            }
            else if (notBigEnough.Count > 0)
            {
                return (Size)Collections.Max(notBigEnough, new CompareSizesByArea());
            }
            else
            {
                Log.Error(TAG, "Couldn't find any suitable preview size");
                return choices[0];
            }
        }

        public static Camera2BasicFragment newInstance()
        {
            return new Camera2BasicFragment();
        }

        /** Layout the preview and buttons. */
        //@Override
        public override View OnCreateView(
            LayoutInflater inflater, ViewGroup container, Bundle savedInstanceState)
        {
            return inflater.Inflate(Resource.Layout.fragment_camera2_basic, container, false);
        }

        /** Connect the buttons to their event handler. */
        //@Override
        public override void OnViewCreated(View view, Bundle savedInstanceState)
        {
            textureView = (AutoFitTextureView)view.FindViewById(Resource.Id.texture);
            textView = (TextView)view.FindViewById(Resource.Id.text);
        }

        /** Load the model and labels. */
        //@Override
        public override void OnActivityCreated(Bundle savedInstanceState)
        {
            base.OnActivityCreated(savedInstanceState);
            try
            {
                 
            }
            catch (Java.IO.IOException e)
            {
                Log.Error(TAG, "Failed to initialize an image classifier.", e);
            }
            startBackgroundThread();
        }

        //@Override
        public override void OnResume()
        {
            base.OnResume();
            startBackgroundThread();

            // When the screen is turned off and turned back on, the SurfaceTexture is already
            // available, and "onSurfaceTextureAvailable" will not be called. In that case, we can open
            // a camera and start preview from here (otherwise, we wait until the surface is ready in
            // the SurfaceTextureListener).
            if (textureView.IsAvailable)
            {
                openCamera(textureView.Width, textureView.Height);
            }
            else
            {
                textureView.SurfaceTextureAvailable += (sender, e) =>
                {
                    openCamera(e.Width, e.Height);
                };
                textureView.SurfaceTextureSizeChanged += (sender, e) =>
                {
                    configureTransform(e.Width, e.Height);
                };
                textureView.SurfaceTextureDestroyed += (sender, e) =>
                {
                    e.Handled = true;
                };
            }
        }

        //@Override
        public override void OnPause()
        {
            closeCamera();
            stopBackgroundThread();
            base.OnPause();
        }

        //@Override
        public override void OnDestroy()
        {
          
            base.OnDestroy();
        }

        /**
         * Sets up member variables related to camera.
         *
         * @param width The width of available size for camera preview
         * @param height The height of available size for camera preview
         */
        private void setUpCameraOutputs(int width, int height)
        {
            Activity activity = Activity;
            CameraManager manager = (CameraManager)activity.GetSystemService(Android.Content.Context.CameraService);

            try
            {
                foreach (String cameraId in manager.GetCameraIdList())
                {
                    CameraCharacteristics characteristics = manager.GetCameraCharacteristics(cameraId);


                    // We don't use a front facing camera in this sample.

                    var facing = (Java.Lang.Integer)characteristics.Get(CameraCharacteristics.LensFacing);
                    if (facing != null && facing.IntValue() == (int)LensFacing.Front)
                    {
                        continue;
                    }

                    StreamConfigurationMap map =
                    (StreamConfigurationMap)characteristics.Get(CameraCharacteristics.ScalerStreamConfigurationMap);
                    if (map == null)
                    {
                        continue;
                    }

                    // // For still image captures, we use the largest available size.
                    Size largest =
                      (Size)Collections.Max(
                            Arrays.AsList(map.GetOutputSizes((int)ImageFormat.Jpeg)), new CompareSizesByArea());
                    imageReader =
                        ImageReader.NewInstance(
                            largest.Width, largest.Height, ImageFormat.Jpeg, /*maxImages*/ 2);

                    // Find out if we need to swap dimension to get the preview size relative to sensor
                    // coordinate.
                    var displayRotation = activity.WindowManager.DefaultDisplay.Rotation;// getDefaultDisplay().getRotation();
                                                                                         // noinspection ConstantConditions
                                                                                         /* Orientation of the camera sensor */

                    var sensorOrientation = (int)characteristics.Get(CameraCharacteristics.SensorOrientation);


                    bool swappedDimensions = false;
                    switch (displayRotation)
                    {
                        case SurfaceOrientation.Rotation0:// Surface.ROTATION_0:
                        case SurfaceOrientation.Rotation180:
                            if (sensorOrientation == 90 || sensorOrientation == 270)
                            {
                                swappedDimensions = true;
                            }
                            break;
                        case SurfaceOrientation.Rotation90:
                        case SurfaceOrientation.Rotation270:
                            if (sensorOrientation == 0 || sensorOrientation == 180)
                            {
                                swappedDimensions = true;
                            }
                            break;
                        default:
                            Log.Error(TAG, "Display rotation is invalid: " + displayRotation);
                            break;
                    }

                    Point displaySize = new Point();
                    activity.WindowManager.DefaultDisplay.GetSize(displaySize);
                    int rotatedPreviewWidth = width;
                    int rotatedPreviewHeight = height;
                    int maxPreviewWidth = displaySize.X;
                    int maxPreviewHeight = displaySize.Y;

                    if (swappedDimensions)
                    {
                        rotatedPreviewWidth = height;
                        rotatedPreviewHeight = width;
                        maxPreviewWidth = displaySize.Y;
                        maxPreviewHeight = displaySize.X;
                    }

                    if (maxPreviewWidth > MAX_PREVIEW_WIDTH)
                    {
                        maxPreviewWidth = MAX_PREVIEW_WIDTH;
                    }

                    if (maxPreviewHeight > MAX_PREVIEW_HEIGHT)
                    {
                        maxPreviewHeight = MAX_PREVIEW_HEIGHT;
                    }

                    previewSize =
                        chooseOptimalSize(
                           map.GetOutputSizes(Java.Lang.Class.FromType(typeof(SurfaceTexture))), rotatedPreviewWidth, rotatedPreviewHeight, maxPreviewWidth, maxPreviewHeight, largest);

                    // We fit the aspect ratio of TextureView to the size of preview we picked.
                    var orientation = Resources.Configuration.Orientation;
                    if (orientation == Android.Content.Res.Orientation.Landscape)
                    {
                        textureView.setAspectRatio(previewSize.Width, previewSize.Height);
                    }
                    else
                    {
                        textureView.setAspectRatio(previewSize.Height, previewSize.Width);
                    }

                    this.cameraId = cameraId;
                    return;
                }
            }
            catch (CameraAccessException e)
            {
                Log.Error(TAG, "Failed to access Camera", e);
            }
            catch (Java.Lang.NullPointerException e)
            {
                // Currently an NPE is thrown when the Camera2API is used but not supported on the
                // device this code runs.
                //   ErrorDialog.newInstance(getString(R.string.camera_error))
                //    .show(getChildFragmentManager(), FRAGMENT_DIALOG);
            }
        }

        private String[] getRequiredPermissions()
        {
            Activity activity = Activity;
            try
            {
                PackageInfo info =
                    activity.PackageManager

                        .GetPackageInfo(activity.PackageName, PackageInfoFlags.Permissions);
                var ps = info.RequestedPermissions;
                if (ps != null && ps.Count > 0)
                {
                    return ps.ToArray();
                }
                else
                {
                    return new String[0];
                }
            }
            catch (Exception e)
            {
                return new String[0];
            }
        }

        /** Opens the camera specified by {@link Camera2BasicFragment#cameraId}. */
        private void openCamera(int width, int height)
        {
            if (!checkedPermissions && !allPermissionsGranted())
            {
                this.RequestPermissions(getRequiredPermissions(), PERMISSIONS_REQUEST_CODE);
                return;
            }
            else
            {
                checkedPermissions = true;
            }
            setUpCameraOutputs(width, height);
            configureTransform(width, height);
            Activity activity = Activity;
            CameraManager manager = (CameraManager)activity.GetSystemService(Android.Content.Context.CameraService);
            try
            {
                if (!cameraOpenCloseLock.TryAcquire(2500, TimeUnit.Milliseconds))
                {
                    throw new Java.Lang.RuntimeException("Time out waiting to lock camera opening.");
                }
                StateCallback2 stateCallback = new StateCallback2();
                stateCallback.OnOpenedHandler += (sender, e) =>
                {
                    cameraOpenCloseLock.Release();
                    cameraDevice = e;
                    createCameraPreviewSession();
                };

                stateCallback.OnDisconnectedHandler += (sender, e) =>
                {
                    cameraOpenCloseLock.Release();
                    e.Close();
                    cameraDevice = null;
                };
                stateCallback.OnErrorHandler += (sender, e) =>
                {
                    cameraOpenCloseLock.Release();
                    e.Item1.Close();
                    cameraDevice = null;
                    Activity activity1 = Activity;
                    if (null != activity1)
                    {
                        activity1.Finish();
                    }
                };

                manager.OpenCamera(cameraId, stateCallback, backgroundHandler);
            }
            catch (CameraAccessException e)
            {
                Log.Error(TAG, "Failed to open Camera", e);
            }
            catch (Java.Lang.InterruptedException e)
            {
                throw new Java.Lang.RuntimeException("Interrupted while trying to lock camera opening.", e);
            }
        }

        private bool allPermissionsGranted()
        {
            foreach (String permission in getRequiredPermissions())
            {
                if (Activity.CheckSelfPermission(permission) != Permission.Granted)
                {
                    return false;
                }
            }
            return true;
        }
        public override void OnRequestPermissionsResult(int requestCode, string[] permissions, [GeneratedEnum] Permission[] grantResults)
        {
            base.OnRequestPermissionsResult(requestCode, permissions, grantResults);
        }


        /** Closes the current {@link CameraDevice}. */
        private void closeCamera()
        {
            try
            {
                cameraOpenCloseLock.Acquire();
                if (null != captureSession)
                {
                    captureSession.Close();
                    captureSession = null;
                }
                if (null != cameraDevice)
                {
                    cameraDevice.Close();
                    cameraDevice = null;
                }
                if (null != imageReader)
                {
                    imageReader.Close();
                    imageReader = null;
                }
            }
            catch (Java.Lang.InterruptedException e)
            {
                throw new Java.Lang.RuntimeException("Interrupted while trying to lock camera closing.", e);
            }
            finally
            {
                cameraOpenCloseLock.Release();
            }
        }

        /** Starts a background thread and its {@link Handler}. */
        private void startBackgroundThread()
        {
            backgroundThread = new HandlerThread(HANDLE_THREAD_NAME);
            backgroundThread.Start();
            backgroundHandler = new Handler(backgroundThread.Looper);
            lock (@lock)
            {
                runClassifier = true;
            }
            backgroundHandler.Post(periodicClassify);
        }

        /** Stops the background thread and its {@link Handler}. */
        private void stopBackgroundThread()
        {
            backgroundThread.QuitSafely();
            try
            {
                backgroundThread.Join();
                backgroundThread = null;
                backgroundHandler = null;
                lock (@lock)
                {
                    runClassifier = false;
                }
            }
            catch (Java.Lang.InterruptedException e)
            {
                Log.Error(TAG, "Interrupted when stopping background thread", e);
            }
        }

        /** Takes photos and classify them periodically. */
        private Java.Lang.Runnable periodicClassify = null;


        /** Creates a new {@link CameraCaptureSession} for camera preview. */
        private void createCameraPreviewSession()
        {
            try
            {
                SurfaceTexture texture = textureView.SurfaceTexture;
                //  assert texture != null;

                // We configure the size of default buffer to be the size of camera preview we want.
                texture.SetDefaultBufferSize(previewSize.Width, previewSize.Height);

                // This is the output Surface we need to start preview.
                Surface surface = new Surface(texture);

                // We set up a CaptureRequest.Builder with the output Surface.
                previewRequestBuilder = cameraDevice.CreateCaptureRequest(CameraTemplate.Preview);
                previewRequestBuilder.AddTarget(surface);
                StateCallback call = new StateCallback();

                call.OnConfiguredHandler += (sender, e) =>
                {

                    if (null == cameraDevice)
                    {
                        return;
                    }

                    // When the session is ready, we start displaying the preview.
                    captureSession = e;// cameraCaptureSession;
                    try
                    {
                        // Auto focus should be continuous for camera preview.
                        previewRequestBuilder.Set(
                            CaptureRequest.ControlAeMode,
                              (int)ControlAFMode.ContinuousPicture);

                        // ly, we start displaying the camera preview.
                        previewRequest = previewRequestBuilder.Build();
                        CaptureCallback captureCallback = new CaptureCallback();

                        captureSession.SetRepeatingRequest(previewRequest, captureCallback, backgroundHandler);
                    }
                    catch (CameraAccessException ex)
                    {
                        Log.Error(TAG, "Failed to set up config to capture Camera", e);
                    }
                };


                // Here, we create a CameraCaptureSession for camera preview.
                cameraDevice.CreateCaptureSession(new System.Collections.Generic.List<Surface>() { surface }, call, null);
            }
            catch (CameraAccessException e)
            {
                Log.Error(TAG, "Failed to preview Camera", e);
            }
        }

        /**
         * Configures the necessary {@link android.graphics.Matrix} transformation to `textureView`. This
         * method should be called after the camera preview size is determined in setUpCameraOutputs and
         * also the size of `textureView` is fixed.
         *
         * @param viewWidth The width of `textureView`
         * @param viewHeight The height of `textureView`
         */
        private void configureTransform(int viewWidth, int viewHeight)
        {
            Activity activity = Activity;
            if (null == textureView || null == previewSize || null == activity)
            {
                return;
            }
            var rotation = activity.WindowManager.DefaultDisplay.Rotation;
            Matrix matrix = new Matrix();
            RectF viewRect = new RectF(0, 0, viewWidth, viewHeight);
            RectF bufferRect = new RectF(0, 0, previewSize.Height, previewSize.Width);
            float centerX = viewRect.CenterX();
            float centerY = viewRect.CenterY();
            if (SurfaceOrientation.Rotation90 == rotation || rotation == SurfaceOrientation.Rotation270)
            {
                bufferRect.Offset(centerX - bufferRect.CenterX(), centerY - bufferRect.CenterY());
                matrix.SetRectToRect(viewRect, bufferRect, Matrix.ScaleToFit.Fill);
                float scale =
                   Math.Max(
                       (float)viewHeight / previewSize.Height,
                       (float)viewWidth / previewSize.Width);
                matrix.PostScale(scale, scale, centerX, centerY);
                matrix.PostRotate(90 * ((int)rotation - 2), centerX, centerY);
            }
            else if (rotation == SurfaceOrientation.Rotation180)
            {
                matrix.PostRotate(180, centerX, centerY);
            }
            textureView.SetTransform(matrix);
        }

        /** Classifies a frame from the preview stream. */
        //private void classifyFrame()
        //{
        //    //if (classifier == null || Activity == null || cameraDevice == null)
        //    //{
        //    //    showToast("Uninitialized Classifier or invalid context.");
        //    //    return;
        //    //}
        //    //Bitmap bitmap = textureView.GetBitmap(classifier.ImageSizeX, classifier.ImageSizeY);
        //    //String textToShow = classifier.ClassifyFrame(bitmap);
        //    //bitmap.Recycle();
        //    //showToast(textToShow);
        //}

    }


    public class ErrorDialog : DialogFragment
    {
        private static String ARG_MESSAGE = "message";
        public static ErrorDialog newInstance(String message)
        {
            ErrorDialog dialog = new ErrorDialog();
            Bundle args = new Bundle();
            args.PutString(ARG_MESSAGE, message);
            dialog.Arguments = (args);
            return dialog;
        }

        public override Dialog OnCreateDialog(Bundle savedInstanceState)
        {
            Activity activity = Activity;

            return new AlertDialog.Builder(activity).SetMessage(Arguments.GetString(ARG_MESSAGE)).SetPositiveButton(Android.Resource.String.Ok, (sender, e) =>
            {
                activity.Finish();

            }).Create();
        }

    }
    public class StateCallback2 : CameraDevice.StateCallback
    {
        public event EventHandler<Tuple<CameraDevice, CameraError>> OnErrorHandler;
        public event EventHandler<CameraDevice> OnOpenedHandler;
        public event EventHandler<CameraDevice> OnDisconnectedHandler;
        public override void OnDisconnected(CameraDevice camera)
        {
            //  throw new NotImplementedException();
            OnDisconnectedHandler?.Invoke(this, camera);
        }

        public override void OnError(CameraDevice camera, [GeneratedEnum] CameraError error)
        {
            var e = Tuple.Create<CameraDevice, CameraError>(camera, error);
            //  throw new NotImplementedException();
            OnErrorHandler?.Invoke(this, e);
        }

        public override void OnOpened(CameraDevice camera)
        {
            //throw new NotImplementedException();
            OnOpenedHandler?.Invoke(this, camera);
        }
    }


    public class StateCallback : CameraCaptureSession.StateCallback
    {
        public event EventHandler<CameraCaptureSession> OnConfiguredHandler;
        public StateCallback()
        {
        }
        public override void OnConfigured(CameraCaptureSession cameraCaptureSession)
        {
            if (OnConfiguredHandler != null)
            {
                OnConfiguredHandler.Invoke(this, cameraCaptureSession);
            }
        }

        public override void OnConfigureFailed(CameraCaptureSession session)
        {
            //throw new NotImplementedException();
        }
    }

    public class CaptureCallback : CameraCaptureSession.CaptureCallback
    {

        public override void OnCaptureProgressed(CameraCaptureSession session, CaptureRequest request, CaptureResult partialResult)
        {
            // base.OnCaptureProgressed(session, request, partialResult);
        }

        public override void OnCaptureCompleted(CameraCaptureSession session, CaptureRequest request, TotalCaptureResult result)
        {
            //base.OnCaptureCompleted(session, request, result);
        }


    }
}